package com.kingwang.demo.springsecuritytokendemo.security;

import com.kingwang.demo.springsecuritytokendemo.security.exception.InvalidTokenTypeException;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.util.Assert;

/**
 * 针对TokenBasedAuthenticationToken的AuthenticationProvider
 *
 * @author King
 */
public class TokenBasedAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    /**
     * 用户缓存
     */
    @Getter
    @Setter
    private UserCache userCache = new NullUserCache();

    private UserDetailsService userDetailsService;

    /**
     * Token Session管理器
     */
    private TokenSessionManager sessionManager;

    /**
     * 是否隐藏用户不存在的提示
     */
    @Getter
    @Setter
    private boolean hideUserNotFoundExceptions = true;

    @Getter
    @Setter
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    public TokenBasedAuthenticationProvider(UserDetailsService userDetailsService,
                                            TokenSessionManager sessionManager) {
        this.userDetailsService = userDetailsService;
        this.sessionManager = sessionManager;
    }

    /**
     * Spring初始化Bean属性注入点
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        doAfterPropertiesSet();
    }

    /**
     * Spring初始化Bean属性注入点
     */
    protected void doAfterPropertiesSet() {
    }

    @Override
    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    /**
     * 认证逻辑
     *
     * @param authentication 待认证的信息
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String tokenType = authentication.getPrincipal() == null ? "" : authentication.getName();

        // 如果Token类型不正确，抛出异常
        if (!isValidTokenType(tokenType)) {
            throw new InvalidTokenTypeException("Token类型 [" + tokenType + "] 非法！");
        }

        TokenBasedAuthenticationToken authenticationToken = (TokenBasedAuthenticationToken) authentication;

        String sessionId = this.sessionManager.generateSessionId(authenticationToken.getTokenType(), authenticationToken.getToken());


        TokenSession session = this.sessionManager.getSession(sessionId);
        if (session == null || this.sessionManager.isExpired(session)) {
            // Session不合法，销毁Session，抛出未通过认证异常
            if (session != null) {
                session.destroy();
            }

            throwBadCredentialsException();
        }

        boolean cacheWasUsed = true;
        UserDetails user = session.getUserDetails();

        // 检查Session中是否存在用户信息
        if (user == null) {
            // 检查用户缓存中是否存在对应Token的用户信息
            user = this.userCache.getUserFromCache(session.getUsername());

            if (user == null) {
                cacheWasUsed = false;

                try {
                    // 如果用户缓存中不存在相关用户，使用UserDetailsService获取用户
                    user = this.userDetailsService.loadUserByUsername(session.getUsername());
                } catch (AuthenticationException ex) {
                    if (this.hideUserNotFoundExceptions && isUsernameNotFoundException(ex)) {
                        throwBadCredentialsException();
                    } else {
                        throw ex;
                    }
                }

                if (user == null) {
                    if (this.hideUserNotFoundExceptions) {
                        throwBadCredentialsException();
                    } else {
                        throw new UsernameNotFoundException("用户不存在！");
                    }
                }
            }
        }

        try {
            preAuthenticate(authenticationToken, user);
            additionalAuthenticationChecks(authenticationToken, user);
        } catch (
                AuthenticationException ex) {
            if (this.hideUserNotFoundExceptions && isUsernameNotFoundException(ex)) {
                throwBadCredentialsException();
            } else {
                throw ex;
            }
        }

        postAuthenticate(authenticationToken, user);

        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        return createSuccessAuthentication(authenticationToken, user);
    }

    /**
     * 创建验证成功的Token
     *
     * @param authentication 验证用的Token
     * @param user           用户信息
     * @return
     */
    protected Authentication createSuccessAuthentication(TokenBasedAuthenticationToken authentication, UserDetails user) {
        String tokenType = authentication.getTokenType();
        String token = authentication.getToken();

        // 生成Token并保存
        String sessionId = this.sessionManager.generateSessionId(tokenType, token);

        TokenSession tokenSession = this.sessionManager.getSession(sessionId);
        if (tokenSession == null) {
            tokenSession = this.sessionManager.createSession(tokenType, authentication, user);
        } else {
            this.sessionManager.refreshSession(tokenSession);
        }

        // 返回认证成功结果
        TokenBasedAuthenticationToken result = new TokenBasedAuthenticationToken(
                tokenType,
                token,
                this.authoritiesMapper.mapAuthorities(user.getAuthorities())
        );

        result.setAuthenticated(true);
        result.setDetails(user);
        // 从Session中将额外属性还原回Token
        tokenSession.getAttributes().forEach((name, value) -> {
            result.setAttribute(name, value);
        });

        return result;
    }

    /**
     * 默认允许所有类型的Token，Token类型不能为空
     *
     * @param tokenType Token类型
     * @return Token类型是否合法
     */
    protected boolean isValidTokenType(String tokenType) {
        return StringUtils.isNotBlank(tokenType);
    }

    /**
     * 注入点：在进行验证之前执行
     */
    protected void preAuthenticate(TokenBasedAuthenticationToken authentication, UserDetails userDetails) throws AuthenticationException {
    }

    /**
     * 注入点：额外的验证逻辑
     *
     * @param authentication
     * @param userDetails
     */
    protected void additionalAuthenticationChecks(TokenBasedAuthenticationToken authentication,
                                                  UserDetails userDetails) throws AuthenticationException {
    }

    /**
     * 注入点：在验证成功后执行
     *
     * @param authentication
     * @param userDetails
     */
    protected void postAuthenticate(TokenBasedAuthenticationToken authentication, UserDetails userDetails) {
    }

    /**
     * 抛出用户认证凭据非法异常
     *
     * @throws BadCredentialsException
     */
    private static void throwBadCredentialsException() throws BadCredentialsException {
        throw new BadCredentialsException("用户认证凭据非法！");
    }

    private static boolean isUsernameNotFoundException(RuntimeException exception) {
        return UsernameNotFoundException.class.isAssignableFrom(exception.getClass());
    }

    /**
     * 判断是否使用此Provider处理对应Token
     *
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return TokenBasedAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
